iT邦幫忙

2024 iThome 鐵人賽

DAY 11
0
Modern Web

簡單的事 最困難-Angular動態Form元件系列 第 11

第11天 要改的東西太多了 不如就繼承吧

  • 分享至 

  • xImage
  •  

昨天調整了base-text
今天來調整其他的
調整著調整著覺得
重複的程式碼太多了吧
有沒有辦法將重複的部分共用呢
這邊我們考慮使用extends
用一個基本的模板把大多數會用到的東西寫進去
於是乎我們新增一個base-element.component.ts
這個沒有要產生模板所以直接新增一個空白的程式

// base-element.component.ts

import { Directive, Input } from '@angular/core';
import { ControlValueAccessor } from '@angular/forms';
import { FieldSetting } from './field-setting.model';

@Directive()
export abstract class BaseElementComponent implements ControlValueAccessor
{
    @Input() fieldSetting!: FieldSetting;
    @Input() value: string | string[] = '';

    onChange: any = () => { };
    onTouch: any = () => { };

    writeValue(value: string | string[]): void
    {
        this.value = value;
    }

    registerOnChange(fn: any): void
    {
        this.onChange = fn;
    }

    registerOnTouched(fn: any): void
    {
        this.onTouch = fn;
    }

    setDisabledState(isDisabled: boolean): void
    {
        // 可以根據需求實現禁用邏輯
    }

    valueChange(event: Event): void
    {
        const input = event.target as HTMLInputElement;
        this.value = input.value;
        this.onChange(input.value);
        this.onTouch();
    }
}

由於每個元件都會用到上述這些部分
所以我們把這些都提取出來
由於這個ts沒有模板 所以我們給他@Directive
另外改傳遞control以後 元件只需要知道自己的control不需要知道整體的fieldObj
所以input移除掉fieldObj
欄位變動的方法統一命名為valueChange
如果其他有一些需要調整的 再使用override 去覆寫
抽取模板以後其餘程式會這樣

// field-template.component.html
<div style="padding-bottom: 8px">
  <span>
    {{ fieldSetting.cname }}
    <span *ngIf="fieldSetting.required" style="color: red">*</span>:
  </span>
  <!-- 傳入control 移除fieldObj -->
  <app-field [fieldSetting]="fieldSetting" [control]="control"></app-field>
</div>

// field.component.ts
    // 傳入control 移除fieldObj
    @Input() fieldSetting!: FieldSetting;
    @Input() control!: FormControl;
    
// field.component.html
  <!-- 傳入control 移除fieldObj -->
<div [ngSwitch]="fieldSetting.inputType">
  <div *ngSwitchCase="'text'">
    <app-base-text
      [fieldSetting]="fieldSetting"
      [formControl]="control"
    ></app-base-text>
  </div>
  <div *ngSwitchCase="'tel'">
    <app-base-tel
      [fieldSetting]="fieldSetting"
      [formControl]="control"
    ></app-base-tel>
  </div>
  <div *ngSwitchCase="'number'">
    <app-base-number
      [fieldSetting]="fieldSetting"
      [formControl]="control"
    ></app-base-number>
  </div>
  <div *ngSwitchCase="'password'">
    <app-base-password
      [fieldSetting]="fieldSetting"
      [formControl]="control"
    ></app-base-password>
  </div>
  <div *ngSwitchCase="'select'">
    <app-base-select
      [fieldSetting]="fieldSetting"
      [formControl]="control"
    ></app-base-select>
  </div>
  <div *ngSwitchCase="'radio'">
    <app-base-radio
      [fieldSetting]="fieldSetting"
      [formControl]="control"
    ></app-base-radio>
  </div>
  <div *ngSwitchCase="'checkbox'">
    <app-base-checkbox
      [fieldSetting]="fieldSetting"
      [formControl]="control"
    ></app-base-checkbox>
  </div>
</div>

而元件的部分一下子就變得很簡單

// base-text.component.html
<div>
  <input
    type="text"
    placeholder="{{ fieldSetting.placeholder || '文字輸入框' }}"
    [value]="value"
    (input)="valueChange($event)"
  />
</div>

// base-text.compoentn.ts
import { Component, forwardRef } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { BaseElementComponent } from '../base-element.component';

@Component({
    selector: 'app-base-text',
    templateUrl: './base-text.component.html',
    styleUrls: ['./base-text.component.scss'],
    providers: [
        {
            provide: NG_VALUE_ACCESSOR,
            useExisting: forwardRef(() => BaseTextComponent),
            multi: true
        },]
})
export class BaseTextComponent extends BaseElementComponent
{
}

以上以base-text為範例
其餘像是base-number,base-tel,base-password,base-radio內容都差不多
直接繼承BaseElementComponent就好
另外兩個有點不太一樣

// base-select.component.html
// 調整change的方法名
<div>
  <select [value]="value" (change)="valueChange($event)">
    <option *ngFor="let option of fieldSetting.options" [value]="option.value">
      {{ option.label }}
    </option>
  </select>
</div>


// base-select.compoentn.ts
import { Component, forwardRef } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { BaseElementComponent } from '../base-element.component';

@Component({
    selector: 'app-base-select',
    templateUrl: './base-select.component.html',
    styleUrls: ['./base-select.component.scss'],
    providers: [
        {
            provide: NG_VALUE_ACCESSOR,
            useExisting: forwardRef(() => BaseSelectComponent),
            multi: true
        }
    ]
})
export class BaseSelectComponent extends BaseElementComponent
{
    override valueChange(event: Event): void
    {
        const select = event.target as HTMLSelectElement;
        this.value = select.value;
        this.onChange(this.value);
        this.onTouch();
    }
}

這邊其實就是接的型別不一樣改成HTMLSelectElment而以
然而我們來看一下checkbox
因為我們有偷改傳進來的格式
是用字串加上逗號去分隔
所以我們要改傳進來的處理跟回傳的處理兩個地方

// base-checkbox.component.html
<div>
  <label class="checkbox" *ngFor="let option of options">
    <input
      type="checkbox"
      [checked]="option.checked"
      [name]="fieldSetting.name"
      value="{{ option.value }}"
      (change)="valueChange($event)"
    />
    <span> <b></b>{{ option.label }}</span>
  </label>
</div>


// base-checkbox.compoentn.ts
import { Component, forwardRef } from '@angular/core';
import { BaseElementComponent } from '../base-element.component';
import { NG_VALUE_ACCESSOR } from '@angular/forms';

@Component({
    selector: 'app-base-checkbox',
    templateUrl: './base-checkbox.component.html',
    styleUrls: ['./base-checkbox.component.scss'],
    providers: [
        {
            provide: NG_VALUE_ACCESSOR,
            useExisting: forwardRef(() => BaseCheckboxComponent),
            multi: true
        }
    ]
})
export class BaseCheckboxComponent extends BaseElementComponent
{
    checkedValue!: string[];
    options!: any[];

    override writeValue(value: string | string[]): void
    {
        this.value = value;
        this.checkedValue = typeof this.value == 'string' ? this.value.split(',') : this.value;
        this.options = this.fieldSetting.options?.map(opt => { return { ...opt, checked: !!this.checkedValue.find(c => c === opt.value) } }) || []

    }

    override valueChange(event: Event)
    {
        const input = event.target as HTMLInputElement;
        let thisOption = this.options.find(opt => opt.value == input.value);
        thisOption.checked = !thisOption.checked;

        this.checkedValue = this.options.filter(opt => opt.checked).map(opt => opt.value);
        this.value = this.checkedValue.join(',');

        // 最後這邊改為用onChange
        this.onChange(this.value);
        this.onTouch();
    }
}

終於改完今天的部分
寫到一半覺得方向要大改 又多花了一點時間
當初這部分也是折磨我好長一段間
調整了一下寫法變得精簡更多
明天應該就可以開始處理驗證的部分了

今日程式:day11


上一篇
第10天 改寫元件for FormControl
下一篇
第12天 表單檢核Validator(上)
系列文
簡單的事 最困難-Angular動態Form元件14
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言